Data Editing

Tags: v6

Data editing refers to actions such as creating orders, manipulating their contents, reserving tables, registering guests, etc. Each individual action makes a small point change — for example, AddOrderItemProduct adds a dish to the order, while AddOrderItemModifier adds a modifier to the dish. Many actions can lead to inconsistent data states on their own — for instance, if a dish has mandatory modifiers, adding the dish without modifiers would violate the corresponding business rule. By combining these actions, one can add a dish with modifiers to the order. It is important that the set of actions is performed in accordance with the “all or nothing” principle and transitions the data from one consistent state to another consistent state. To ensure transactionality when changing data, the concept of editing sessions is introduced.

Editing Sessions

An editing session is somewhat similar to transactions in databases. All actions, even single ones, are performed within sessions as follows:

  1. An IEditSession session is created by calling PluginContext.Operations.CreateEditSession.
  2. One or more actions are performed within the session.
  3. The changes made are saved using the method PluginContext.Operations.SubmitChanges.

Changes made in the second step are not visible until they are successfully saved. In the third step, either all changes are successfully recorded, or all are rolled back, and an exception is generated.

Synchronization

Access to data is synchronized using locks and revisions. During editing, objects are locked, but it is impossible to manage locks directly through the API — objects are automatically locked when changes are saved (SubmitChanges). This corresponds to the concept of optimistic locking: at the stages of creating an editing session and performing actions, objects are not locked, and when saving the changes made, a check is performed to see if they have been changed by someone else at the same time. With each change, new revisions are assigned to the objects, allowing different versions of the same object to be distinguished. Given the low competition for parallel editing of the same objects, this approach simplifies the programming interface (no lock management methods, no need to worry about their proper release) and ensures data availability (it is impossible to lock an object for a long time, it is impossible to “forget” to release a lock, in case of a plugin crash, the object will not remain in a locked state).

Some implementation features should be taken into account:

Performing Operations in an Uninterrupted Series

Operations (IOperationService) that require synchronization for data editing lock and then unlock objects with each call. Accordingly, when sequentially calling several operations, each of them will independently lock the data, make changes, and then unlock, allowing other requests to edit the same data to “intervene” between operation calls, resulting in some of our operations succeeding while others may fail with errors like EntityAlreadyInUseException, EntityModifiedException, and some operations may become inapplicable considering others’ edits.

For example, in a plugin that receives delivery orders from an external source (website, aggregator), it was necessary to create a delivery with an external prepayment processed. Performing all this atomically within a single editing session is impossible, as processing a payment is an irreversible operation that is performed separately, so first, we create the delivery and fill in its fields, including adding an unprocessed external prepayment (CreateEditSession, CreateDeliveryOrder, AddExternalPaymentItem, etc.), we save these changes (SubmitChanges), and then we attempt to process the prepayment (ProcessPrepay). Between these operations (SubmitChanges and ProcessPrepay), the data is unlocked and available for editing by anyone. Those who are subscribed to delivery changes may receive a notification about the creation of our new delivery and make changes to it before we process the prepayment. Writing code that is not afraid of being interrupted and can continue to work while adapting to others’ edits is labor-intensive. To facilitate the implementation of such scenarios, the ability to perform several operations in one uninterrupted series has been added.

ExecuteContinuousOperation is a special operation within which several other operations can be sequentially executed in one uninterrupted series. In the plugin code, you need to gather a series of operations into one function or lambda and pass it as a callback to the ExecuteContinuousOperation method, which will call this callback, passing it a special instance of the IOperationService designed for uninterrupted operation execution:

PluginContext.Operations.ExecuteContinuousOperation(
    operations =>
    {
        ...
        operations.SubmitChanges(...);
        ...
        operations.ProcessPrepay(...);
        ...
    });

It should be noted that the root operation ExecuteContinuousOperation is called through the common service PluginContext.Operations, while the nested operations are called through the instance of the service obtained by the lambda (named operations in the example above). Technically, operations called through the special instance of the service work exactly the same way, but do not unlock the data afterwards, meaning that each operation requiring synchronization locks the data if they have not already been locked by previous operations, makes changes, and leaves the data locked for subsequent operations. This guarantees that no one else can “steal” the lock and “intervene,” and our subsequent operations on these same objects will not encounter EntityAlreadyInUseException. The data is unlocked when control returns from the lambda.

The following limitations should be considered:

Stubs

Since the results of actions cannot be obtained until the entire session is saved, it is sometimes necessary to refer to an object that is being created but does not yet exist when performing a sequence of actions within a single session. For example, after creating an order, there needs to be a way to add a guest to it, to add a dish to that guest, and to add a modifier to that dish, even though there is as yet no order, guest, or dish. For this purpose, the concept of object stubs is introduced — certain fictitious, yet unambiguous pointers to objects. Object creation actions, such as CreateOrder or AddOrderGuest, return stubs of the type INew...Stub, which can be used within the same session instead of future objects.

Most editing methods accept such stubs as arguments, allowing both existing and new objects to be passed to them. For example, the method SetOrderType accepts IOrderStub, so it can set the type for both an already existing order (IOrder : IOrderStub) and a newly created one (INewOrderStub : IOrderStub).

However, some actions may require strictly one of the two — a new or an existing object; in such cases, the method signature will use not the base type but one of its descendants.

Expected Exceptions

Various exceptions may arise when attempting to save changes. Some of them may indicate an error in the plugin code (for example, ArgumentNullException or ArgumentOutOfRangeException), and it is not recommended to suppress such exceptions (it is better to fix the error in the code). However, some exceptions cannot be anticipated or prevented, and they should be caught and handled correctly:

Syntactic Sugar

Sometimes it is necessary to perform just one action, while the explicit creation of an editing session looks cumbersome. For such cases, helper extension methods have been implemented for IOperationService, which create an editing session, perform a single action, save changes, and return the result of the action. In principle, all of this could have been written manually. It is not recommended to use these wrappers if multiple actions are expected to be executed simultaneously.